package com.fowo.api.common.excel;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fowo.api.common.model.PageSearch;
import com.fowo.api.sys.entity.SysImportTemplate;
import com.fowo.api.sys.model.SysDictAllItem;
import com.fowo.api.sys.service.SysDictService;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 自定义 Excel 导出组件
 * @author yl_ls
 */
@SuppressWarnings("unused")
@Component
public class CustomExcelExport {

    @Resource
    private SysDictService sysDictService;

    /**
     * 按输入的选项运行导出
     * @param options 导出选项
     * @param outputStream 导出流
     */
    public void run(CustomExcelExportOptions options, OutputStream outputStream) {

        ExcelWriterBuilder write = EasyExcel.write(outputStream);
        // 按模版生成 Excel 列头
        List<List<String>> excelHeads = options.getImportTemplate().getSetting().getExcelColumns().stream().map(ec -> Collections.singletonList(ec.getName())).collect(Collectors.toList());
        write.head(excelHeads);
        // Excel 列名称与索引关联表
        Map<String, Integer> columnIndexMappings = new HashMap<>();
        for (int i = 0; i < options.getImportTemplate().getSetting().getExcelColumns().size(); i++) {
            columnIndexMappings.put(options.getImportTemplate().getSetting().getExcelColumns().get(i).getName(), i);
        }
        // 字段名与索引关联表
        Map<String, Integer> fieldIndexMappings = new HashMap<>();
        // 字符串类型的列名集合
        List<Integer> stringNameIndexs = CollUtil.newArrayList();
        for (SysImportTemplate.Mapping mapping : options.getImportTemplate().getSetting().getMappings()) {
            int index = columnIndexMappings.getOrDefault(mapping.getExcelColumnNames().get(0), -1);
            fieldIndexMappings.put(mapping.getField(), index);
            if (CollUtil.isNotEmpty(options.getStringFields()) && options.getStringFields().stream().anyMatch(any -> StrUtil.equalsIgnoreCase(Convert.toStr(any), mapping.getField()))) {
                stringNameIndexs.add(index);
            }
        }
        List<List<Object>> excelData = new ArrayList<>();
        boolean hasChildExport = options.getChildMappingOption() != null &&
                options.getImportTemplate()
                        .getSetting()
                        .getMappings().stream()
                        .anyMatch(f -> f.getField().startsWith(options.getChildMappingOption().fieldName + "."));

        Method pageSearchMethod;
        try {
            pageSearchMethod = options.mainService.getClass().getMethod("pageSearch", options.getSearchParam().getClass());
            PageSearch pageSearch = options.getSearchParam();
            pageSearch.setPageSize(options.batchQuerySize);
            pageSearch.setSelectFields(Collections.singletonList("@export"));
            int current = 0;
            Page<?> page;
            do {
                current++;
                pageSearch.setCurrent(current);
                page = (Page<?>) pageSearchMethod.invoke(options.mainService, pageSearch);
                for (Object record : page.getRecords()) {
                    List<Object> excelRow = new ArrayList<>(excelHeads.size());
                    fillRecordToExcelRow(options, record, excelRow, columnIndexMappings, fieldIndexMappings);
                    excelData.add(excelRow);
                    if (hasChildExport) {
                        runChild(options, record, excelRow, excelData, columnIndexMappings, fieldIndexMappings);
                    }
                }
            } while (current < page.getPages());

        } catch (ReflectiveOperationException e) {
            throw new RuntimeException("无法获取导出服务需要的 pageSearch 方法");
        }

        write.sheet()
                .registerConverter(new ImageConvert())
                .registerWriteHandler(new CustomExcelHandler(stringNameIndexs))
                .doWrite(excelData);
    }

    private void runChild(CustomExcelExportOptions options, Object record, List<Object> parentExcelRow, List<List<Object>> excelData, Map<String, Integer> columnIndexMappings, Map<String, Integer> fieldIndexMappings) {
        int current = 0;
        Page<?> childPage;
        do {
            current++;
            childPage = options.childMappingOption.childPageSearch.search(record, current, options.batchQuerySize);
            for (int i = 0; i < childPage.getRecords().size(); i++) {
                Object childRecord = childPage.getRecords().get(i);
                boolean inParentRow = current == 1 && i == 0;
                List<Object> childCells = inParentRow ? parentExcelRow : new ArrayList<>();
                if (!inParentRow) {
                    // 填充空间
                    while (childCells.size() < options.getImportTemplate().getSetting().getExcelColumns().size()) {
                        childCells.add(null);
                    }
                    // 复制父级字段格子
                    for (Map.Entry<String, Integer> stringIntegerEntry : fieldIndexMappings.entrySet()) {
                        if (stringIntegerEntry.getKey().contains(".")) {
                            continue;
                        }
                        childCells.set(stringIntegerEntry.getValue(), parentExcelRow.get(stringIntegerEntry.getValue()));
                    }
                }
                fillChildRecordToExcelRow(options, record, childRecord, childCells, columnIndexMappings, fieldIndexMappings);
                if (!inParentRow) {
                    excelData.add(childCells);
                }
            }
        } while (current < childPage.getPages());
    }

    private void fillChildRecordToExcelRow(CustomExcelExportOptions options, Object parentRecord, Object record, List<Object> excelRow, Map<String, Integer> columnIndexMappings, Map<String, Integer> fieldIndexMappings) {
        Class<?> recordClass = record.getClass();
        for (SysImportTemplate.Mapping mapping : options.getImportTemplate().getSetting().getMappings()) {
            String excelCol = mapping.getExcelColumnNames().get(0);
            int colIndex = columnIndexMappings.getOrDefault(excelCol, -1);
            if (colIndex == -1) {
                continue;
            }
            String fieldName = mapping.getField();
            String childFieldName = fieldName;
            String[] names = fieldName.split("\\.");
            if (names.length == 2 && names[0].equals(options.childMappingOption.fieldName)) {
                childFieldName = names[1];
            } else {
                continue;
            }
            Field field = ReflectionUtils.findField(recordClass, childFieldName);
            if (field == null) {
                continue;
            }
            field.trySetAccessible();
            Object value = ReflectionUtils.getField(field, record);
            FieldMappingOption fieldMappingOption = options.fieldMappingOptions.get(fieldName);
            if (fieldMappingOption != null) {
                value = runFieldMapping(value, fieldMappingOption, record, options);
            }
            excelRow.set(colIndex, value);
        }
    }

    private void fillRecordToExcelRow(CustomExcelExportOptions options, Object record, List<Object> excelRow, Map<String, Integer> columnIndexMappings, Map<String, Integer> fieldIndexMappings) {
        Class<?> recordClass = record.getClass();
        while (excelRow.size() < options.getImportTemplate().getSetting().getExcelColumns().size()) {
            excelRow.add(null);
        }
        for (SysImportTemplate.Mapping mapping : options.getImportTemplate().getSetting().getMappings()) {
            String excelCol = mapping.getExcelColumnNames().get(0);
            int colIndex = columnIndexMappings.getOrDefault(excelCol, -1);
            if (colIndex == -1) {
                continue;
            }
            String fieldName = mapping.getField();
            Field field = ReflectionUtils.findField(recordClass, fieldName);
            if (field == null) {
                continue;
            }
            field.trySetAccessible();
            Object value = ReflectionUtils.getField(field, record);
            FieldMappingOption fieldMappingOption = options.fieldMappingOptions.get(fieldName);
            if (fieldMappingOption != null) {
                value = runFieldMapping(value, fieldMappingOption, record, options);
            }
            excelRow.set(colIndex, value);
        }
    }

    private Object runFieldMapping(Object value, FieldMappingOption fieldMappingOption, Object record, CustomExcelExportOptions options) {
        if (Objects.isNull(value)) {
            return null;
        }
        if (value instanceof CharSequence && ((CharSequence) value).length() == 0) {
            return "";
        }
        if (StringUtils.hasText(fieldMappingOption.dictCode)) {
            List<SysDictAllItem> dictItems = sysDictService.getDictItems(fieldMappingOption.dictCode);
            if (dictItems != null) {
                final Object[] finalValue = new Object[]{ value };
                dictItems.stream().filter(i -> i.getItemValue().equals(finalValue[0])).findFirst()
                        .ifPresentOrElse(
                                dictItem -> finalValue[0] = dictItem.getItemText(),
                                () -> finalValue[0] = null
                        );
                value = finalValue[0];
            }
        }
        if (fieldMappingOption.converter != null) {
            value = fieldMappingOption.converter.apply(value, record);
        }
        return value;
    }


    /**
     * 自定义 Excel 导出选项
     */
    @Getter
    @Setter
    public static class CustomExcelExportOptions {
        /**
         * 导入模版
         */
        private SysImportTemplate importTemplate;

        /**
         * 输入查询参数
         */
        private PageSearch searchParam;

        /**
         * 主服务类
         */
        private Object mainService;

        /**
         * 分页查询，单次查询大小
         */
        private int batchQuerySize = 1000;

        /**
         * 最大导出 Excel 行数，如果大于此行数，将忽略之后的数据
         */
        private long maxSize = 1048575;

        /**
         * 字段对应选项
         */
        private Map<String, FieldMappingOption> fieldMappingOptions = new HashMap<>();

        /**
         * 添加字段对应选项
         * @param fieldName 字段名
         * @param option 选项
         */
        public void addFieldMappingOption(String fieldName, FieldMappingOption option) {
            fieldMappingOptions.put(fieldName, option);
        }

        /**
         * 类型列表<类型，字段>
         */
        private List<String> stringFields = new ArrayList<>();

        /**
         * 添加字段对应选项
         *
         * @param fieldNames 选项
         */
        public void addStringFields(String ...fieldNames) {
            CollUtil.addAll(stringFields, fieldNames);
        }

        /**
         * 子表选项
         */
        private ChildMappingOption childMappingOption;
    }

    /**
     * 子表字段配置
     */
    @Getter
    @Setter
    @Accessors(chain = true)
    public static class ChildMappingOption {
        /**
         * 子表字段名（不是真实字段，对应导入模版中的前缀）
         */
        private String fieldName;

        /**
         * 子表查询处理
         */
        private ChildPageSearch childPageSearch;
    }

    /**
     * 字段选项
     */
    @Getter
    @Setter
    @Accessors(chain = true)
    public static class FieldMappingOption {
        /**
         * 转换处理函数，将读取的字段值转换到显示
         */
        private FieldMappingConverter converter;

        /**
         * 指定关联的字段代码
         */
        private String dictCode;
    }

    @FunctionalInterface
    public interface FieldMappingConverter {
        String apply(Object value, Object record);
    }

    /**
     * 子表的查询调用处理方法
     */
    @FunctionalInterface
    public interface ChildPageSearch {
        /**
         * 查询子表
         * @param parentRecord 父表记录数据
         * @param current 当前页码
         * @param pageSize 页大小
         * @return 子表分页查询结果
         */
        Page<?> search(Object parentRecord, int current, int pageSize);
    }
}
